//使用axios进行网路请求,文档：http://www.axios-js.com
import axios from 'axios';
import {ToolEncode} from './index';

//请求方式与数据类型
const Con = {get: 'get', post: 'post', json: 'json', form: 'form', result1: 1, result2: 2, result3: 3};

/**
 * 网络请求：基于axios框架封装
 * 1.封装公共参数，并使用AES、RSA加密；
 * 2.自动对密码等二次加密
 * 3.自动处理网络错误
 * 4.自动获取mock数据；
 * 5.自动从后端获取公钥
 */
export default class ToolRequest {
  constructor(data = {}) {
    this.configWeb = data;
    this.isMock = data.mode === 'mock'; //是否mock环境
    this.isBeta = data.mode === 'beta'; //是否测试环境
    this.isPrd = data.mode === 'prod'; //是否生产环境
    this.RSA_KEY = data.publicKey; //RSA公钥:从配置文件获取(不推荐)
    this.AES_KEY = ToolEncode.getKeyAES(); //AES密钥:随机生成的

    this.safeParams = {'login.do': ['password']}; //需要单独加密的参数

    //请求配置
    this.configHttp = {
      dataName: 'data', //返回结果的属性名
      msgName: 'desc', //返会消息的属性名
      codeName: 'status', //返回码的属性名
      codeSuccess: 1000, //响应成功的返回码
      codeLogin: 1001, //登录超时的返回码
      codeTimeout: 1002 //响应超时的返回码
    };

    this.initRequest();
  }

  /**
   * 初始化框架的公共信息：比如baseURL、超时时间、token等
   * 注意：登录后需要调用initRequest()，来设置token。
   */
  initRequest(data = {}) {
    const {timeout = 30, token} = data;
    axios.defaults.timeout = timeout * 1000; //设置超时时间，单位为毫秒
    axios.defaults.headers.common.token = token; //设置token
  }

  /**
   * 普通的网络请求
   * @param url {String} 请求地址
   * @param params {Object} 请求参数。
   * @param options {Object} 请求配置。格式如下：
   * {
   *  typeRequest, //请求方式，为'get'或'post'，默认值：'get'。
   *  typeParam,//请求参数的类型，为'json'或'form'：为'json'代表提交json数据；为'form'代表提交表单数据。默认值：'json'。
   *  typeResult,//请求结果的类型，为1、2、3：1是返回请求结果（网络配置+后台结果）；2是返回后台结果（业务状态+业务结果）；3是返回业务结果（前端需要的数据）。默认值：3。
   *  isEncode,//是否添加公共参数并加密，格式：boolean。
   *  isShowError,//是否提示错误，格式：boolean。默认值：true。
   *  isCheckStatus,//是否处理业务状态，格式：boolean。默认值：true。
   *  ...//axios的标准配置数据
   * }
   */
  ajax(url, params = {}, options = {}) {
    let optionDefault = {typeRequest: Con.get, typeResult: Con.result3, isShowError: true, isCheckStatus: true};
    options = Object.assign(optionDefault, options);
    return this.requestCommon({url, params, options});
  }

  /**
   * get请求，会对参数加密、加签。
   * 注意：只会对params进行加密，对url中包含的参数不会加密。
   * @param url 请求地址
   * @param params 请求参数,格式为对象
   * @param options 请求配置，同上。
   */
  get(url, params = {}, options = {}) {
    let optionDefault = {typeResult: Con.result3, isEncode: false, isShowError: true, isCheckStatus: true};
    options = Object.assign(optionDefault, options, {typeRequest: Con.get});
    return this.requestCommon({url, params, options});
  }

  /**
   * post请求，会对参数加密、加签。
   * 支持JSON数据与表单数据提交，通过options.method进行配置。
   * @param url 请求地址
   * @param params 请求参数：格式为对象。
   * @param options 请求配置，同上。
   */
  post(url, params = {}, options = {}) {
    let optionDefault = {
      typeParam: Con.json,
      typeResult: Con.result3,
      isEncode: false, // 参数是否加密
      isShowError: true,
      isCheckStatus: true
    };
    options = Object.assign(optionDefault, options, {typeRequest: Con.post});
    return this.requestCommon({url, params, options});
  }

  /**
   * 通用请求
   * @param requestConfig 请求信息。见上。
   */
  requestCommon(requestConfig = {}) {
    let {url, params, options} = requestConfig;

    //如果需要获取公钥，就拦截请求
    if (this.needIntercept(requestConfig)) {
      return this.interceptRequest(requestConfig);
    }

    this.log(url, params, options); //开发、测试环境打印请求参数
    this.getMock(requestConfig); //判断并配置mock请求
    this.addHost(requestConfig); //添加host
    this.encodeAlone(requestConfig); //对特定参数单独加密
    this.encode(requestConfig); //添加公共参数并加密加签

    return this.requestAxios(requestConfig)
      .catch((e) => this.checkHttp(e, requestConfig))
      .then(this.decode)
      .then(this.checkBusiness);
  }

  /**
   * 判断是否拦截请求
   * 如果参数需要加密，并且公钥为空，就拦截请求
   * @param requestConfig 请求信息。
   */
  needIntercept(requestConfig) {
    if (this.isMock || this.RSA_KEY || !requestConfig.options.isEncode) return false;

    return true;
  }

  /**
   * 先获取公钥，然后执行原来的请求
   * @param requestConfig 请求信息。
   */
  interceptRequest(requestConfig) {
    //获取公钥。
    this.RSA_KEY = this.configWeb.publicKey;
    //执行原来的请求
    return this.requestCommon(requestConfig);
  }

  /**
   * 根据url，从Mock文件中获取响应数据。如果没有Mock数据，则使用测试数据。
   * 注意：需要现在mock目录下创建mock文件，并定义url的响应数据。
   * @param requestConfig 请求信息。
   */
  getMock(requestConfig) {
    if (!this.isMock) return;

    const mock = this.getMockData();
    const data = mock[requestConfig.url]; //从mock文件中获取响应数据
    if (!data) return;

    requestConfig.options.isEncode = false;
    //用于axios处理自定义请求
    requestConfig.options.adapter = function (config) {
      return new Promise(function (resolve) {
        const response = {
          data,
          config,
          status: 200,
          statusText: 'OK'
        };

        console.log(requestConfig.url, data);

        resolve(response);
      });
    };
  }

  /**
   * 给url添加域名
   * @param requestConfig 请求信息。
   */
  addHost(requestConfig) {
    let url = requestConfig.url;
    let host = this.configWeb.host;
    //如果url以http开头，则不需要添加域名
    if (url && url.startsWith('http')) {
      return;
    }

    if ((this.isBeta || this.isPrd) && host && url) {
      //url的首字母
      let start = url.charAt(0);
      //host的尾字母
      let end = host.charAt(host.length - 1);
      if (start === '/' && end === '/') {
        url = host + url.substr(1, url.length - 1);
      } else if (start !== '/' && end !== '/') {
        url = host + '/' + url;
      } else {
        url = host + url;
      }
    }
    requestConfig.url = url;
  }

  /**
   * 对特定参数单独加密
   * @param requestConfig 请求信息。
   */
  encodeAlone(requestConfig = {}) {
    let {safeParams, RSA_KEY} = this;
    let {url, params, options} = requestConfig;
    if (!url || !safeParams || !options.isEncode) return;

    //后台接口名
    let name = this.getUrlMethod(url);
    let needParams = safeParams[name];
    Object.keys(params).forEach((key) => {
      //如果参数需要加密,就调用加密方法
      if (needParams && needParams.indexOf(key) > -1) {
        params[key] = ToolEncode.encodeLogin(params[key], RSA_KEY, window.timestamp);
      }
    });
    requestConfig.params = params;
  }

  /**
   * 从url中获取后台接口名
   * @param url 请求地址
   */
  getUrlMethod(url) {
    if (!url) return;

    //方法名在url中的起点
    let start = url.lastIndexOf('/') + 1;
    //方法名在url中的终点
    let end = url.indexOf('?');
    if (end < 0) {
      end = url.length;
    }
    //后台接口名
    return url.substring(start, end);
  }

  /**
   * 处理参数：添加公共参数、AES加密、RSA加密、SHA签名
   * 密码等敏感参数,需要提前单独加密.
   * @param requestConfig 请求请求信息。
   */
  encode(requestConfig = {}) {
    let {AES_KEY, RSA_KEY} = this;
    let {publicParams, secret} = this.configWeb;
    let {options = {}, params} = requestConfig;
    if (!options.isEncode) return;

    //当前时间
    let time = new Date();
    //对所有参数进行AES加密
    let requestData = ToolEncode.encodeAES(params, AES_KEY);
    //对AES的密钥进行RSA加密
    let encodeKey = ToolEncode.encodeRSA(AES_KEY, RSA_KEY);
    //请求唯一标识
    let requestId = time.getTime().toString();
    //时间戳
    let timestamp = this.getTimestamp(time);

    //加密结果
    let result = {secret, encodeKey, ...publicParams, requestData, requestId, timestamp};
    //签名
    result.sign = ToolEncode.signature(result);
    //防止私密信息，需要删除私密信息。
    delete result.secret;
    requestConfig.params = result;
  }

  /**
   * 使用axios作为请求框架
   * @param requestConfig 请求信息。
   */
  requestAxios(requestConfig = {}) {
    let {url, params, options = {}} = requestConfig;
    let {typeRequest, typeParam, typeResult, isEncode, isShowError, isCheckStatus, ...other} = options;
    let config = {url, method: typeRequest, ...other};

    //处理get请求
    if (typeRequest === Con.get) {
      config.url = this.connectUrlParams(url, params);

      //处理其他请求
    } else {
      config.headers = config.headers || {};
      //注意：请求参数为null时，Content-Type默认为application/x-www-form-urlencoded
      config.data = params || {};
      //处理form格式的数据。默认为json格式。
      if (typeParam && typeParam.toLowerCase() === Con.form) {
        config.headers['Content-Type'] = 'multipart/form-data';
        config.data = this.getFormData(params);
      }
    }

    //返回结果会返回请求结果与requestConfig，因为需要根据requestConfig判断是否需要解密
    return axios(config).then((result) => ({result, requestConfig}));
  }

  /**
   * 拼接url与参数
   * 参考:https://www.jianshu.com/p/7c097051b5dc
   * @param url 请求地址
   * @param params 请求参数
   */
  connectUrlParams(url, params) {
    if (!params) return;

    let paramsArray = [];
    //拼接参数
    Object.keys(params).forEach((key) => {
      let value = params[key];
      if (typeof value === 'object') {
        value = JSON.stringify(value);
      }
      paramsArray.push(key + '=' + encodeURIComponent(value));
    });
    if (paramsArray.length > 0) {
      if (!url) {
        url = paramsArray.join('&');
      } else if (url.search(/\?/) === -1) {
        url += '?' + paramsArray.join('&');
      } else {
        url += '&' + paramsArray.join('&');
      }
    }
    return url;
  }

  /**
   * 将请求参数转化为FormData对象,用于post请求
   * @param params 请求参数
   */
  getFormData(params) {
    let formData = null;
    if (params) {
      formData = new FormData();
      //填充formData
      Object.keys(params).forEach((key) => formData.append(key, params[key]));
    }
    return formData;
  }

  /**
   * 对响应结果解密
   * @param data 响应结果
   */
  decode = (data) => {
    let {dataName} = this.configHttp;
    let {result = {}, requestConfig = {}} = data;
    let resultData = result.data[dataName];
    if (requestConfig.options.isEncode && resultData) {
      resultData = ToolEncode.decodeAES(resultData, this.AES_KEY);
      result.data[dataName] = JSON.parse(resultData);
    }
    return data;
  };

  /**
   * 处理Http状态码：axios会自动检查http状态码，状态码不正确，则抛出错误；网络超时，则抛出错误。
   * axios通过validateStatus函数检查http状态码，可以在config中配置validateStatus。默认的validateStatus为：
   *  status => status >= 200 && status < 300;
   * @param e 错误信息
   * @param requestConfig 请求参数
   */
  checkHttp(e, requestConfig) {
    let {isShowError} = requestConfig.options;
    if (isShowError) this.showError(e.message);

    throw {isHttpError: true, dataError: e}; //抛出网络异常
  }

  /**
   * 处理业务逻辑的状态码
   * @param data 后台响应数据
   */
  checkBusiness = (data) => {
    let {result, requestConfig} = data;
    let {typeResult, isShowError, isCheckStatus} = requestConfig.options;
    let {codeName, codeSuccess, codeLogin, dataName, msgName} = this.configHttp;
    let code = result.data[codeName]; //业务状态
    let resultData; //返回结果

    if (typeResult === Con.result1) {
      resultData = result;
    } else if (typeResult === Con.result2) {
      resultData = result.data;
    } else {
      resultData = result.data && result.data[dataName];
    }

    if (code) {
      if (code === codeSuccess) {
        return resultData;
      } else {
        if (isShowError) this.showError(result.data[msgName]); //提示错误信息

        //处理业务状态
        if (isCheckStatus) {
          if (code === codeLogin) this.goLogin(); //跳转登录页
        }

        throw {isBusinessError: true, dataError: result}; //抛出业务异常
      }
    } else {
      return resultData;
    }
  };

  /**
   * 打印请求地址与参数
   */
  log(url, params) {
    !this.isPrd && console.log(url, params); //非生产环境，打印log
  }

  /**
   * 提示错误信息
   * @param msg {*} 错误消息
   */
  showError(msg) {}

  /**
   * 跳转登录页
   */
  goLogin() {}

  /**
   * 获取格式化的时间戳
   * @param date 日期
   */
  getTimestamp(date) {
    return date.toLocaleString();
  }

  /**
   * 获取Mock数据
   */
  getMockData() {}
}
